Leptos를 사용한 구글 지도 애플리케이션 개발

Leptos를 사용한 구글 지도 애플리케이션 개발

1. 서론: Rust, Leptos, 그리고 웹 지도의 만남

현대 웹 개발의 패러다임은 사용자에게 더 빠르고 안정적인 인터랙션을 제공하는 방향으로 끊임없이 진화하고 있다. 이러한 흐름 속에서 Rust 언어는 타입 안정성과 메모리 안전성이라는 강력한 무기를 바탕으로 웹 프론트엔드 개발의 새로운 가능성을 제시한다. Leptos는 바로 이 Rust의 잠재력을 웹에 온전히 구현하기 위해 설계된 프레임워크다.1 Leptos의 핵심 철학은 가상 DOM(Virtual DOM)을 사용하지 않고, 상태 변화가 필요한 DOM 노드만을 정밀하게 갱신하는 ‘세분화된 반응성(fine-grained reactivity)’ 모델에 기반한다.3 이 접근 방식은 불필요한 연산을 최소화하여 기존 JavaScript 프레임워크들을 능가하는 압도적인 성능을 목표로 한다.5

한편, 웹 기반 지도 서비스 영역에서 Google Maps JavaScript API는 부동의 산업 표준으로 자리매김하고 있다. 전 세계를 아우르는 방대한 지리 데이터, 마커, 데이터 레이어, 스트리트 뷰 등 풍부한 시각화 도구, 그리고 강력한 지오코딩 및 경로 탐색 기능은 복잡한 위치 기반 서비스를 구축하는 데 필수적인 요소들을 제공한다.6

이 두 강력한 기술의 만남은 엄청난 시너지를 약속하지만, 동시에 근본적인 기술적 과제를 내포한다. 본 보고서의 목표는 단순히 두 기술을 연동하는 코드를 나열하는 것을 넘어, 두 세계의 본질적인 ’패러다임 불일치’를 해결하기 위한 견고하고 확장 가능한 아키텍처 패턴을 수립하는 데 있다. Leptos는 상태를 중심으로 UI를 선언적으로 기술하는 반응형 세계에 속해 있는 반면, Google Maps API는 특정 DOM 요소의 제어권을 완전히 위임받아 내부 상태를 스스로 관리하는 명령형 객체 모델을 따른다.8 이는 ’누가 상태와 DOM의 제어권을 갖는가’라는 근본적인 충돌을 야기한다. 이 문제를 해결하는 과정은 단순한 API 호출을 넘어, 두 시스템 간의 상태를 안정적으로 동기화하고, 성능 저하 없이 상호작용을 중재하는 정교한 ’어댑터(Adapter)’를 설계하는 여정이 될 것이다.

본 보고서는 개발 환경 구축부터 시작하여 기본 지도 렌더링, 반응형 상태 관리, 고급 기능 통합 및 프로덕션 환경을 위한 최적화에 이르기까지, Leptos와 Google Maps API를 통합하는 전 과정을 심층적으로 탐색한다. 이를 통해 개발자는 Rust와 Leptos가 제공하는 성능과 안정성을 최대한 활용하면서, 동시에 JavaScript 생태계의 강력한 자산을 통합하는 실전적인 해법을 얻게 될 것이다.

2. 부: 개발 환경 구축 및 지도 렌더링의 첫걸음

견고한 애플리케이션은 체계적인 개발 환경 위에서 시작된다. 이 장에서는 Leptos 프로젝트를 초기화하고, Google Maps API 사용을 위한 준비를 마친 뒤, 두 기술 스택을 연결하는 핵심적인 연동 계층을 구축하여 화면에 첫 지도를 띄우는 과정을 단계별로 상세히 다룬다.

2.1 Leptos 프로젝트 설정 및 구조 분석

Leptos 애플리케이션 개발을 시작하기 위해 가장 먼저 필요한 도구는 cargo-leptos이다. 이는 프로젝트 생성, 빌드, 실행, 배포 등 개발 전반의 과정을 자동화하여 복잡성을 크게 낮춰주는 필수 CLI 툴이다.3

먼저, 다음 명령어를 통해 cargo-leptos를 시스템에 설치한다.

Bash

cargo install cargo-leptos

설치가 완료되면, 새로운 Client-Side Rendering(CSR) 프로젝트를 생성한다. CSR 모드는 서버 없이 순수하게 브라우저 환경에서만 동작하는 SPA(Single Page Application)를 구축할 때 적합하며, 외부 JavaScript 라이브러리와의 통합을 시작하기에 가장 직관적인 환경을 제공한다.

Bash

cargo leptos new --template-name csr leptos-google-maps-app
cd leptos-google-maps-app

이 명령은 leptos-google-maps-app이라는 이름의 디렉터리를 생성하고 그 안에 CSR 템플릿 기반의 프로젝트 구조를 만든다. 생성된 프로젝트의 주요 파일과 디렉터리는 다음과 같다.

  • Cargo.toml: 프로젝트의 의존성 및 메타데이터를 정의하는 Rust의 패키지 매니페스트 파일이다. leptos 크레이트가 csr 피처 플래그와 함께 등록되어 있음을 확인할 수 있다. 이 플래그는 Leptos가 클라이언트 측 렌더링 모드로 동작하도록 지시하는 핵심 설정이다.9
  • src/main.rs: 애플리케이션의 진입점(entry point)이다. main 함수 내에서 Leptos 애플리케이션의 루트 컴포넌트를 DOM의 <body> 태그에 마운트하는 코드가 포함되어 있다.
  • index.html: 웹 애플리케이션의 기본 HTML 파일이다. 초기에는 비어있는 <body> 태그만 존재하며, trunk가 빌드 과정에서 컴파일된 WASM 로더 스크립트를 자동으로 주입한다.
  • style/main.scss: 애플리케이션의 스타일을 정의하는 SCSS 파일이다.
  • Trunk.toml: trunk 빌드 도구의 설정 파일이다. 빌드 결과물이 생성될 디렉터리, public URL 경로 등을 지정할 수 있다.10

이 프로젝트는 trunk라는 WASM 웹 애플리케이션 번들러를 사용하여 빌드 및 실행된다.11 trunk는 Rust 코드를 WebAssembly로 컴파일하고, 필요한 JavaScript ‘접착제(glue)’ 코드를 생성하며, index.html에 이를 삽입하여 최종적으로 브라우저에서 실행 가능한 형태로 만들어주는 역할을 담당한다. 개발 서버를 실행하려면 다음 명령어를 입력한다.

Bash

cargo leptos watch

이 명령어는 소스 코드의 변경 사항을 감지하여 자동으로 재컴파일하고 웹 브라우저를 실시간으로 새로고침해주는 편리한 개발 환경을 제공한다.

2.2 Google Maps API 키 발급 및 보안 설정

Google Maps API를 사용하기 위해서는 먼저 유효한 API 키를 발급받아야 한다. 이는 Google의 서비스 사용량을 추적하고 인증하는 역할을 하는 중요한 자격 증명이다.

  1. API 키 생성: Google Cloud Console에 접속하여 새 프로젝트를 생성하거나 기존 프로젝트를 선택한다. ‘API 및 서비스’ 대시보드에서 ’API 및 서비스 사용 설정’을 클릭하고, ’Maps JavaScript API’를 검색하여 활성화한다. 필요에 따라 ‘Geocoding API’, ‘Places API’ 등 추가적인 API도 함께 활성화할 수 있다.13 API를 활성화한 후, ‘사용자 인증 정보’ 페이지로 이동하여 ‘사용자 인증 정보 만들기’ > ’API 키’를 선택하면 새로운 키가 생성된다.
  2. API 키 보안 설정: 생성된 API 키를 코드에 그대로 사용하는 것은 심각한 보안 위협을 초래할 수 있다. 악의적인 사용자가 키를 탈취하여 무단으로 사용하면 막대한 요금이 부과될 수 있다. 따라서 프로덕션 환경에서는 반드시 API 키에 제한을 설정해야 한다.14
  • 애플리케이션 제한: 키를 사용할 수 있는 주체를 한정하는 가장 중요한 보안 설정이다. 웹 애플리케이션의 경우 ’HTTP 리퍼러(웹사이트)’를 선택하고, 애플리케이션이 호스팅될 도메인 주소를 등록한다. 예를 들어, example.com에서만 API를 사용하도록 하려면 *.example.com/*과 같이 와일드카드를 사용하여 등록할 수 있다. localhost에서의 개발을 위해 http://localhost:*/* 또는 http://127.0.0.1:*/*를 추가하는 것도 잊지 말아야 한다.16
  • API 제한: 해당 키로 호출할 수 있는 API의 종류를 제한하는 설정이다. ’키 제한’을 선택하고 드롭다운 목록에서 ’Maps JavaScript API’와 같이 애플리케이션에서 실제로 사용하는 API들만 선택한다. 이는 키가 유출되더라도 공격자가 다른 비싼 API(예: Routes API)를 무단으로 호출하는 것을 방지한다.14

이러한 보안 설정은 초기 개발 단계부터 적용하는 것이 좋다. 이는 단순한 권장 사항을 넘어, 안정적이고 안전한 프로덕션 애플리케이션을 구축하기 위한 필수적인 아키텍처 설계의 일부이다. 클라이언트 측에 API 키를 노출하는 방식은 근본적인 한계를 가지므로, 3부에서는 서버 함수를 통해 이 문제를 더욱 안전하게 해결하는 아키텍처를 다룰 것이다.

2.3 JavaScript-Rust 연동: wasm-bindgen을 이용한 브릿지 구축

Rust 코드가 컴파일된 WebAssembly(WASM)는 자체적으로 브라우저의 DOM API나 외부 JavaScript 라이브러리에 직접 접근할 수 없다. WASM은 격리된 샌드박스 환경에서 실행되기 때문이다. 이 간극을 메우기 위해 Rust-WASM 생태계는 wasm-bindgen이라는 강력한 도구를 제공한다.17 wasm-bindgen은 Rust의 타입 시스템과 JavaScript의 동적 타입 시스템 사이의 다리 역할을 하며, 양방향 함수 호출이 가능하도록 ‘접착제’ 코드를 자동으로 생성한다.18

Google Maps API를 Leptos에서 사용하려면, 먼저 JavaScript 측에서 API 스크립트를 비동기적으로 로드하고 초기화하는 함수를 만든 뒤, wasm-bindgen을 통해 이 함수를 Rust 코드에서 호출할 수 있도록 해야 한다.

먼저, 프로젝트 루트에 js 디렉터리를 만들고 그 안에 maps_loader.js 파일을 생성한다. 이 파일은 Google Maps API 스크립트를 동적으로 로드하고, 로드가 완료되면 Promise를 반환하는 역할을 한다.

JavaScript

// js/maps_loader.js
let apiLoaded = false;
let loadingPromise = null;

// Google Maps API 스크립트를 비동기적으로 로드하는 함수
export function loadGoogleMapsApi(apiKey) {
if (apiLoaded) {
return Promise.resolve();
}

if (loadingPromise) {
return loadingPromise;
}

loadingPromise = new Promise((resolve, reject) => {
// 전역 콜백 함수 정의
window.onGoogleMapsApiLoaded = () => {
console.log("Google Maps API loaded successfully.");
apiLoaded = true;
loadingPromise = null;
resolve();
};

const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&callback=onGoogleMapsApiLoaded`;
script.async = true;
script.defer = true;
script.onerror = () => {
console.error("Google Maps API failed to load.");
loadingPromise = null;
reject(new Error('Google Maps API failed to load.'));
};
document.head.appendChild(script);
});

return loadingPromise;
}

// 지도 객체를 생성하고 반환하는 래퍼 함수
export function initMap(element, options) {
if (window.google && window.google.maps) {
return new window.google.maps.Map(element, options);
} else {
throw new Error("Google Maps API is not loaded yet.");
}
}

이제 이 JavaScript 함수들을 Rust에서 호출할 수 있도록 wasm-bindgen 설정을 추가한다. src/map_component.rs와 같은 별도의 모듈 파일을 만들고 다음 코드를 작성한다.

Rust

// src/map_component.rs
use leptos::*;
use wasm_bindgen::prelude::*;
use web_sys::HtmlElement;

// wasm-bindgen 어트리뷰트를 사용하여 JavaScript 모듈과 함수를 임포트한다.
#[wasm_bindgen(module = "/js/maps_loader.js")]
extern "C" {
// JavaScript 함수의 이름과 Rust 함수 이름을 맞춘다.
// 비동기 JS 함수는 async fn으로 선언한다.
#[wasm_bindgen(js_name = loadGoogleMapsApi)]
async fn load_google_maps_api(apiKey: String);

// initMap 함수는 지도 객체를 반환하지만, 복잡한 JS 객체는 일단 JsValue로 받는다.
#[wasm_bindgen(js_name = initMap)]
fn init_map(element: &HtmlElement, options: &JsValue) -> JsValue;
}

#[wasm_bindgen(module = "...")] 어트리뷰트는 trunk가 번들링할 때 참조할 JavaScript 파일의 경로를 지정한다.19 extern "C" 블록 내부에 선언된 함수들은 Rust 컴파일러에게 이 함수들의 구현이 외부에 있음을 알린다.17 js_name 어트리뷰트는 JavaScript에서의 실제 함수 이름과 Rust에서 사용할 함수 이름이 다를 경우 매핑해주는 역할을 한다. 비동기 JavaScript 함수(Promise를 반환하는 함수)는 Rust에서 async fn으로 선언하여 .await 키워드를 통해 자연스럽게 처리할 수 있다.

2.4 지도 컨테이너 컴포넌트 설계 및 초기화

Leptos와 같은 선언적 프레임워크에서 외부 명령형 라이브러리를 통합할 때 가장 중요한 개념 중 하나는 컴포넌트의 생명주기를 이해하는 것이다. Leptos의 컴포넌트 함수는 UI 구조를 설정하기 위해 단 한 번만 실행된다. 이는 매 렌더링마다 재실행되는 React의 함수형 컴포넌트와는 근본적으로 다른 동작 방식이다.20 따라서 컴포넌트 함수 본문이 실행되는 시점에는 view! 매크로에 정의된 DOM 요소가 아직 실제 DOM에 마운트되지 않은 상태다.

Google Maps API는 지도를 렌더링할 특정 <div> DOM 요소를 필요로 하므로, 지도 초기화 코드는 반드시 해당 <div>가 DOM에 추가된 이후에 실행되어야 한다. 또한, API 스크립트 자체가 비동기적으로 로드되므로, 스크립트 로딩이 완료될 때까지 기다려야 한다. 이 두 가지 비동기적 제약을 해결하기 위해 Leptos는 NodeRefcreate_resource, create_effect라는 강력한 도구를 제공한다.

  1. NodeRef로 DOM 요소 참조 얻기: create_node_ref를 사용하여 <div> 요소에 대한 참조 변수를 생성하고, view! 매크로 내에서 _ref 어트리뷰트를 통해 이를 바인딩한다. 이렇게 하면 컴포넌트가 렌더링된 후 node_ref.get()을 통해 해당 DOM 요소에 접근할 수 있다.18
  2. create_resource로 API 비동기 로드: create_resource는 비동기 작업을 감싸고 그 상태(로딩 중, 완료, 실패)를 반응형으로 추적하는 데 사용된다. 1.3에서 정의한 load_google_maps_api 함수를 create_resource의 fetcher로 전달하여 API 로딩 과정을 관리한다.22
  3. create_effect로 초기화 실행: create_effect는 의존하는 반응형 값(Signal, Resource 등)이 변경될 때마다 내부의 코드를 실행하는 훅이다. map_api_loaded 리소스와 map_container_ref를 이 effect의 의존성으로 삼아, “API 로딩이 완료되고 <div> 요소가 마운트되었을 때“라는 조건을 만족하면 지도 초기화 함수(init_map)를 호출하도록 구현한다.

다음은 이 패턴을 적용한 GoogleMap 컴포넌트의 전체 코드다.

Rust

// src/map_component.rs (이어서)
use leptos::html::Div;
use serde_json::json;

#[component]
pub fn GoogleMap() -> impl IntoView {
// 1. 지도가 렌더링될 div에 대한 참조를 생성한다.
let map_container_ref = create_node_ref::<Div>();

// 환경 변수나 설정 파일에서 API 키를 안전하게 가져오는 것이 좋다.
// 여기서는 예시를 위해 하드코딩한다.
let api_key = "YOUR_API_KEY_HERE".to_string();

// 2. create_resource를 사용하여 Google Maps API를 비동기적으로 로드한다.
let map_api_resource = create_resource(
|
| (), // 이 리소스는 다른 Signal에 의존하지 않으므로, 한 번만 실행된다.
move |_| {
let key = api_key.clone();
async move {
load_google_maps_api(key).await;
}
},
);

// 3. create_effect를 사용하여 API 로드 및 div 마운트가 완료되면 지도를 초기화한다.
create_effect(move |_| {
// map_api_resource.get()이 Some(())이면 로딩이 완료된 것이다.
if map_api_resource.get().is_some() {
if let Some(div_element) = map_container_ref.get() {
// 지도 옵션을 JSON 형태로 준비한다.
// serde_json을 사용하면 복잡한 JS 객체를 쉽게 만들 수 있다.
let options = json!({
"center": { "lat": 37.5665, "lng": 126.9780 }, // 서울 중심
"zoom": 12
});

// serde_json::Value를 JsValue로 변환한다.
let js_options = JsValue::from_serde(&options).unwrap();

// JS 함수를 호출하여 지도를 초기화한다.
let _map_instance = init_map(&div_element, &js_options);
// 추후 지도를 제어하기 위해 _map_instance를 Signal이나 Context에 저장해야 한다.
}
}
});

view! {
<div _ref=map_container_ref style="width: 100%; height: 600px;">
// 리소스의 상태에 따라 로딩 메시지를 표시한다.
{move |

| match map_api_resource.get() {
None => view! { <p>"Loading Google Maps API..."</p> }.into_view(),
Some(_) => view! { <p>"Map Initializing..."</p> }.into_view(),
}}
</div>
}
}

이 코드를 src/app.rs에서 호출하고 cargo leptos watch를 실행하면, 브라우저 화면에 서울을 중심으로 하는 Google 지도가 성공적으로 렌더링되는 것을 확인할 수 있다. 이로써 두 기술 스택을 연결하는 첫 번째 관문을 통과했다. 다음 장에서는 이 정적인 지도를 Leptos의 반응형 시스템과 연동하여 동적으로 제어하는 방법을 탐구한다.

3. 부: 반응형 상태 관리와 양방향 지도 제어

1부에서 성공적으로 지도를 렌더링했지만, 이는 아직 시작에 불과하다. 진정한 웹 애플리케이션은 사용자의 입력과 데이터의 변화에 따라 동적으로 반응해야 한다. 이 장에서는 Leptos의 핵심인 반응형 시스템(Reactivity System)을 Google Maps API와 통합하여, 두 시스템 간에 상태를 원활하게 동기화하는 ‘양방향 지도 제어’ 아키텍처를 구축한다.

3.1 Leptos Signal을 이용한 지도 상태 관리

애플리케이션의 상태를 관리하는 일관된 전략은 코드의 예측 가능성과 유지보수성을 결정하는 핵심 요소다. Leptos-Google Maps 애플리케이션에서는 Leptos의 Signal을 ’진실의 원천(source of truth)’으로 삼아야 한다. 즉, 지도의 중심 좌표(center)나 확대 수준(zoom)과 같은 핵심 상태는 Google 지도 객체 내부가 아닌, Leptos의 반응형 시스템 내에서 관리되어야 한다.

이를 위해 읽기와 쓰기가 모두 가능한 RwSignal을 사용한다.23

Rust

// src/map_controller.rs
use leptos::*;
use serde::{Deserialize, Serialize};

#
pub struct LatLng {
pub lat: f64,
pub lng: f64,
}

// 지도 상태를 관리하는 구조체
#[derive(Clone, Copy)]
pub struct MapState {
pub center: RwSignal<LatLng>,
pub zoom: RwSignal<u8>,
// 지도 객체 인스턴스를 저장할 Signal
// Option으로 감싸서 초기화 전 상태를 표현한다.
pub map_instance: RwSignal<Option<JsValue>>,
}

impl MapState {
pub fn new() -> Self {
Self {
center: create_rw_signal(LatLng { lat: 37.5665, lng: 126.9780 }),
zoom: create_rw_signal(12),
map_instance: create_rw_signal(None),
}
}
}

MapState 구조체를 만들어 관련 Signal들을 그룹화하면 상태 관리가 용이해진다. 이 MapState 인스턴스를 provide_context를 통해 하위 컴포넌트들에게 제공하면, 애플리케이션 어디서든 지도 상태에 접근하고 수정할 수 있게 된다.9

이제 이 Signal들을 UI 입력 요소와 연결하여 사용자가 직접 지도 상태를 제어할 수 있도록 만들어 보자.

Rust

#[component]
fn MapController(map_state: MapState) -> impl IntoView {
view! {
<div>
<h2>"Map Controls"</h2>
<div>
<label>"Latitude: "</label>
<input
type="number"
prop:value=move |

| map_state.center.get().lat
on:input=move |ev| {
let new_lat = event_target_value(&ev).parse::<f64>().unwrap_or_default();
map_state.center.update(|center| center.lat = new_lat);
}
/>
</div>
<div>
<label>"Longitude: "</label>
<input
type="number"
prop:value=move |

| map_state.center.get().lng
on:input=move |ev| {
let new_lng = event_target_value(&ev).parse::<f64>().unwrap_or_default();
map_state.center.update(|center| center.lng = new_lng);
}
/>
</div>
<div>
<label>"Zoom: "</label>
<input
type="range"
min="1" max="20"
prop:value=move |

| map_state.zoom.get()
on:input=move |ev| {
let new_zoom = event_target_value(&ev).parse::<u8>().unwrap_or(1);
map_state.zoom.set(new_zoom);
}
/>
<span>{move |

| map_state.zoom.get()}</span>
</div>
</div>
}
}

이 컨트롤러 컴포넌트는 MapState의 Signal 값을 prop:value를 통해 입력 필드에 표시하고, on:input 이벤트를 통해 사용자 입력을 받아 다시 Signal을 업데이트한다. 이로써 UI와 Leptos 상태 간의 양방향 바인딩이 완성되었다. 하지만 아직 이 상태 변화가 실제 지도에 반영되지는 않는다.

3.2 ‘Reactive Map Controller’ 패턴 구현 (Leptos -> Google Maps)

이제 Leptos의 상태 변화를 Google 지도 객체에 전달하는 단방향 데이터 흐름을 구축할 차례다. 이는 Leptos의 반응형 시스템이 외부의 명령형 라이브러리를 ’제어’하는 핵심 로직이다. 이 패턴을 ’Reactive Map Controller’라고 명명할 수 있다.

create_effect를 사용하여 centerzoom Signal의 변화를 감지하고, 값이 변경될 때마다 wasm-bindgen으로 연결된 JavaScript 헬퍼 함수를 호출하여 지도 객체의 상태를 명령형으로 업데이트한다.

먼저, js/maps_controller.js 파일을 만들어 지도 객체를 제어하는 함수들을 추가한다.

JavaScript

// js/maps_controller.js
export function setMapCenter(map, center) {
if (map && typeof map.setCenter === 'function') {
map.setCenter(center);
}
}

export function setMapZoom(map, zoom) {
if (map && typeof map.setZoom === 'function') {
map.setZoom(zoom);
}
}

그리고 Rust 측에서 이 함수들을 호출할 수 있도록 바인딩을 추가한다.

Rust

// src/map_component.rs (바인딩 추가)
#[wasm_bindgen(module = "/js/maps_controller.js")]
extern "C" {
#[wasm_bindgen(js_name = setMapCenter)]
fn set_map_center(map: &JsValue, center: &JsValue);

#[wasm_bindgen(js_name = setMapZoom)]
fn set_map_zoom(map: &JsValue, zoom: u8);
}

이제 GoogleMap 컴포넌트 내에서 create_effect를 사용하여 이들을 연결한다.

Rust

// src/map_component.rs (GoogleMap 컴포넌트 수정)
#[component]
pub fn GoogleMap(map_state: MapState) -> impl IntoView {
//... (NodeRef, Resource 등 기존 코드)

// 지도 초기화 부분 수정
create_effect(move |_| {
if map_api_resource.get().is_some() {
if let Some(div_element) = map_container_ref.get() {
// 초기 옵션은 Signal의 현재 값으로 설정
let options = json!({
"center": map_state.center.get_untracked(),
"zoom": map_state.zoom.get_untracked()
});
let js_options = JsValue::from_serde(&options).unwrap();
let map_instance = init_map(&div_element, &js_options);
// 생성된 지도 인스턴스를 Signal에 저장
map_state.map_instance.set(Some(map_instance));
}
}
});

// Center Signal 변경 감지 Effect
create_effect(move |_| {
// Signal을 읽어서 의존성을 등록한다.
let center = map_state.center.get();
if let Some(map) = map_state.map_instance.get() {
let js_center = JsValue::from_serde(&center).unwrap();
set_map_center(&map, &js_center);
}
});

// Zoom Signal 변경 감지 Effect
create_effect(move |_| {
let zoom = map_state.zoom.get();
if let Some(map) = map_state.map_instance.get() {
set_map_zoom(&map, zoom);
}
});

//... (view! 매크로)
}

이로써 UI 컨트롤러에서 Signal 값을 변경하면, create_effect가 이를 감지하여 JavaScript 함수를 호출하고, 실제 지도가 업데이트되는 데이터 흐름이 완성되었다.

하지만 여기서 한 가지 중요한 성능 문제를 고려해야 한다. 사용자가 줌 슬라이더를 빠르게 드래그하면 zoom Signal이 초당 수십 번 변경될 수 있다. 이 때마다 create_effect가 실행되어 WASM-JS 경계를 넘나드는 고비용의 setMapZoom 함수를 호출한다면, 애플리케이션의 반응성이 크게 저하될 수 있다.25 이는 Leptos의 세분화된 반응성이 오히려 성능 저하를 유발하는 역설적인 상황이다.

이 문제를 해결하기 위해 leptos-use 라이브러리의 watch_debounced와 같은 유틸리티를 사용할 수 있다.26 이는 특정 시간(예: 200ms) 동안 추가적인 변경이 없을 때만 콜백 함수를 실행하여 불필요한 호출을 걸러내는 기법이다.

Rust

use leptos_use::watch_debounced;

//...
// Zoom Signal 변경 감지 Effect 대신 watch_debounced 사용
watch_debounced(
move |

| map_state.zoom.get(),
move |zoom, _, _| {
if let Some(map) = map_state.map_instance.get_untracked() {
set_map_zoom(&map, *zoom);
}
},
200.0, // 200ms 디바운스
);

이러한 최적화는 반응형 시스템과 외부 라이브러리를 통합할 때 반드시 고려해야 할 중요한 설계 원칙이다.

3.3 이벤트 리스너 연동 (Google Maps -> Leptos)

이제 반대 방향의 데이터 흐름, 즉 사용자가 직접 지도를 드래그하거나 줌 레벨을 변경했을 때 그 결과를 다시 Leptos의 Signal에 반영하는 로직을 구현할 차례다. 이를 위해서는 Google Maps API가 제공하는 이벤트 시스템을 활용해야 한다.27

핵심 과제는 Rust에서 작성된 클로저(closure)를 JavaScript 이벤트 리스너로 전달하는 것이다. wasm-bindgen은 이를 위해 Closure::wrap이라는 기능을 제공한다. 이는 Rust 클로저를 JavaScript가 호출할 수 있는 형태로 감싸주는 역할을 한다.

먼저, 이벤트 리스너를 등록하고 해제하는 JavaScript 헬퍼 함수를 만든다.

JavaScript

// js/maps_events.js
export function addMapListener(map, eventName, callback) {
if (map && typeof map.addListener === 'function') {
return map.addListener(eventName, callback);
}
return null;
}

// 지도에서 현재 center 값을 가져오는 함수
export function getMapCenter(map) {
return map.getCenter().toJSON();
}

// 지도에서 현재 zoom 값을 가져오는 함수
export function getMapZoom(map) {
return map.getZoom();
}

Rust 측에서 바인딩을 추가하고, Closure를 사용하여 이벤트 핸들러를 구현한다.

Rust

// src/map_component.rs (바인딩 추가)
use wasm_bindgen::closure::Closure;

#[wasm_bindgen(module = "/js/maps_events.js")]
extern "C" {
#[wasm_bindgen(js_name = addMapListener, catch)]
fn add_map_listener(map: &JsValue, event_name: &str, callback: &Closure<dyn FnMut()>) -> Result<JsValue, JsValue>;

#[wasm_bindgen(js_name = getMapCenter, catch)]
fn get_map_center(map: &JsValue) -> Result<JsValue, JsValue>;

#[wasm_bindgen(js_name = getMapZoom, catch)]
fn get_map_zoom(map: &JsValue) -> Result<u8, JsValue>;
}

//... GoogleMap 컴포넌트 내부에 추가
create_effect(move |_| {
if let Some(map) = map_state.map_instance.get() {
// 'center_changed' 이벤트 핸들러
let map_clone_center = map.clone();
let map_state_clone_center = map_state;
let center_changed_closure = Closure::wrap(Box::new(move |

| {
if let Ok(js_center) = get_map_center(&map_clone_center) {
if let Ok(center) = js_center.into_serde::<LatLng>() {
// Signal을 업데이트한다. get_untracked로 무한 루프 방지
if map_state_clone_center.center.get_untracked()!= center {
map_state_clone_center.center.set(center);
}
}
}
}) as Box<dyn FnMut()>);

// 'zoom_changed' 이벤트 핸들러
let map_clone_zoom = map.clone();
let map_state_clone_zoom = map_state;
let zoom_changed_closure = Closure::wrap(Box::new(move |

| {
if let Ok(zoom) = get_map_zoom(&map_clone_zoom) {
if map_state_clone_zoom.zoom.get_untracked()!= zoom {
map_state_clone_zoom.zoom.set(zoom);
}
}
}) as Box<dyn FnMut()>);

// 리스너 등록
add_map_listener(&map, "center_changed", &center_changed_closure).unwrap();
add_map_listener(&map, "zoom_changed", &zoom_changed_closure).unwrap();

// 중요: 컴포넌트가 unmount될 때 메모리 누수를 방지하기 위해 closure를 해제해야 한다.
on_cleanup(move |

| {
//.forget()을 호출하여 Rust가 closure의 메모리를 해제하지 않도록 한다.
// JavaScript GC가 이를 관리하게 된다.
center_changed_closure.forget();
zoom_changed_closure.forget();
});
}
});

이벤트 핸들러 내부에서 Signal을 업데이트할 때 .get_untracked()를 사용하여 현재 값을 비교하는 것이 중요하다. 만약 .get()을 사용하면 해당 Signal에 대한 의존성이 등록되어, Signal 업데이트 -> create_effect (Leptos->GMaps) 실행 -> 지도 상태 변경 -> 이벤트 발생 -> 이벤트 핸들러(GMaps->Leptos) 실행 -> Signal 업데이트… 와 같은 무한 루프에 빠질 수 있다.

on_cleanup 내에서 closure.forget()을 호출하는 것은 wasm-bindgen의 메모리 관리 모델에서 매우 중요한 부분이다. 이를 통해 Rust의 소유권 시스템이 클로저를 해제하는 것을 막고, JavaScript의 가비지 컬렉터가 이벤트 리스너와 함께 클로저를 관리하도록 위임한다.

이제 사용자가 UI 컨트롤러를 조작하든, 지도를 직접 조작하든, 양쪽의 상태는 항상 일관되게 동기화된다. 이로써 견고한 양방향 데이터 바인딩 아키텍처가 완성되었다.

3.4 동적 마커 관리

지도 애플리케이션의 핵심 기능은 지도 위에 위치 데이터를 시각화하는 것이다. Leptos의 반응형 리스트 렌더링 기능을 활용하면 동적으로 마커를 추가, 제거, 업데이트하는 기능을 효율적으로 구현할 수 있다.

먼저, 위치 데이터 목록을 RwSignal로 관리한다.

Rust

// src/map_controller.rs
#
pub struct Location {
pub id: u32,
pub name: String,
pub position: LatLng,
}

// MapState에 추가
pub locations: RwSignal<Vec<Location>>,

// MapState::new()에 초기화 추가
locations: create_rw_signal(vec![
Location { id: 1, name: "N서울타워".to_string(), position: LatLng { lat: 37.5512, lng: 126.9882 } },
Location { id: 2, name: "경복궁".to_string(), position: LatLng { lat: 37.5796, lng: 126.9770 } },
]),

다음으로, 이 목록을 렌더링하기 위한 <For> 컴포넌트와 각 마커를 담당할 <MapMarker> 자식 컴포넌트를 만든다.

Rust

// js/maps_marker.js
export function createMarker(map, options) {
return new google.maps.Marker({ map,...options });
}

export function removeMarker(marker) {
marker.setMap(null);
}

Rust

// src/map_marker.rs
#[wasm_bindgen(module = "/js/maps_marker.js")]
extern "C" {
#[wasm_bindgen(js_name = createMarker)]
fn create_marker(map: &JsValue, options: &JsValue) -> JsValue;

#[wasm_bindgen(js_name = removeMarker)]
fn remove_marker(marker: &JsValue);
}

#[component]
fn MapMarker(
map_instance: ReadSignal<Option<JsValue>>,
location: Location,
) -> impl IntoView {
create_effect(move |_| {
if let Some(map) = map_instance.get() {
let options = json!({
"position": location.position,
"title": location.name
});
let js_options = JsValue::from_serde(&options).unwrap();
let marker = create_marker(&map, &js_options);

// 컴포넌트가 DOM에서 제거될 때 마커도 지도에서 제거한다.
on_cleanup(move |

| {
remove_marker(&marker);
});
}
});

// 이 컴포넌트는 UI를 렌더링하지 않고, 부수 효과(side effect)만 발생시킨다.
// 따라서 빈 view를 반환한다.
view! {}
}

<MapMarker> 컴포넌트는 create_effect를 사용하여 마커를 생성하고, on_cleanup을 통해 마커를 제거한다. 이는 외부 리소스의 생명주기를 Leptos 컴포넌트의 생명주기와 일치시키는 매우 중요한 패턴이다.

마지막으로, GoogleMap 컴포넌트에서 <For>를 사용하여 locations Signal을 렌더링한다.

Rust

// src/map_component.rs (GoogleMap 컴포넌트의 view! 매크로 내부)
<For
each=move |

| map_state.locations.get()
key=|location| location.id
children=move |location| {
view! {
<MapMarker map_instance=map_state.map_instance.read_only() location=location />
}
}
/>

이제 map_state.locations Signal에 새로운 Location을 추가하거나 기존 Location을 제거하면, <For> 컴포넌트가 변경 사항을 감지하여 <MapMarker> 컴포넌트를 동적으로 생성하거나 제거하고, 그 결과 실제 지도 위의 마커가 반응형으로 업데이트된다.

3.5 사용자 위치 추적 및 지도 연동

사용자의 현재 위치를 지도에 표시하는 것은 많은 위치 기반 서비스의 핵심 기능이다. leptos-use 라이브러리는 브라우저의 Geolocation API를 반응형으로 사용할 수 있게 해주는 use_geolocation 훅을 제공하여 이 기능을 쉽게 구현할 수 있도록 돕는다.29

use_geolocation 훅은 사용자의 좌표(coords), 위치 파악 시각(located_at), 에러(error) 등을 담고 있는 반응형 Signal들을 반환한다.

Rust

// src/user_location.rs
use leptos::*;
use leptos_use::{use_geolocation_with_options, UseGeolocationReturn, UseGeolocationOptions};

#[component]
pub fn UserLocationTracker(map_state: MapState) -> impl IntoView {
let options = UseGeolocationOptions {
enable_high_accuracy: true,
..Default::default()
};
let UseGeolocationReturn { coords,.. } = use_geolocation_with_options(options);

create_effect(move |_| {
// coords Signal은 Option<Coordinates> 타입이다.
if let Some(coords) = coords.get() {
let new_center = LatLng {
lat: coords.latitude(),
lng: coords.longitude(),
};
// 사용자 위치로 지도 중심을 이동시킨다.
map_state.center.set(new_center);
// 더 부드러운 경험을 위해 줌 레벨도 조정할 수 있다.
map_state.zoom.set(15);
}
});

view! {}
}

UserLocationTracker 컴포넌트를 애플리케이션에 추가하면, 사용자가 위치 정보 제공에 동의하는 즉시 coords Signal이 업데이트된다. create_effect는 이 변화를 감지하여 map_statecenter Signal을 갱신하고, 이는 다시 2.2에서 구축한 ’Reactive Map Controller’를 통해 실제 지도의 중심을 사용자의 현재 위치로 이동시킨다. 이는 외부 라이브러리에서 제공하는 훅과 자체 상태 관리 로직을 자연스럽게 결합하는 훌륭한 예시이다.

4. 부: 고급 기능 통합 및 최적화

기본적인 지도 렌더링과 양방향 제어를 넘어, 프로덕션 수준의 애플리케이션은 비동기 데이터 로딩, 라우팅, 사용자 상호작용 등 더 복잡한 요구사항을 가진다. 이 장에서는 Leptos가 제공하는 고급 기능들을 활용하여 지도 애플리케이션을 더욱 풍부하고 견고하게 만드는 방법과 프로덕션을 위한 최종 고려사항들을 다룬다.

4.1 비동기 데이터 로딩과 지도 시각화

실제 애플리케이션에서는 지도에 표시할 위치 데이터를 서버 API를 통해 비동기적으로 가져와야 하는 경우가 대부분이다. Leptos는 이러한 비동기 작업을 처리하기 위해 create_resource, create_local_resource, create_action 등 다양한 프리미티브를 제공한다. 각 프리미티브는 고유한 사용 사례와 장단점을 가지므로, 상황에 맞는 올바른 도구를 선택하는 것이 중요하다.

프리미티브주요 사용 사례서버-클라이언트 직렬화트리거 방식
create_resourceSSR 환경에서 초기 데이터 로딩, 의존성 변경 시 자동 재요청지원 (성능 이점)의존하는 Signal 변경 시
create_local_resourceCSR 전용, 브라우저 API 접근, 직렬화 불필요 데이터미지원의존하는 Signal 변경 시
create_action사용자 이벤트(버튼 클릭, 폼 제출) 기반 데이터 변경(POST, PUT)서버 액션과 결합 시 지원.dispatch() 수동 호출

초기 지도에 표시될 대량의 데이터를 로드하는 시나리오에서는 create_resource가 가장 적합하다. 특히 서버 사이드 렌더링(SSR) 환경에서 create_resource는 서버에서 데이터 로딩을 시작하고 그 결과를 클라이언트로 직렬화하여 전달하므로, 사용자가 페이지를 더 빨리 볼 수 있게 해주는 결정적인 성능 이점을 제공한다.22

다음은 create_resource를 사용하여 서버에서 위치 데이터 목록을 가져오는 예제다.

Rust

// src/api.rs
// 서버 함수를 정의한다. 이 코드는 서버에서만 실행된다.
#[server(GetLocations, "/api")]
pub async fn get_locations() -> Result<Vec<Location>, ServerFnError> {
// 실제로는 데이터베이스 쿼리 등이 들어간다.
Ok(vec![
Location { id: 3, name: "롯데월드타워".to_string(), position: LatLng { lat: 37.5126, lng: 127.1025 } },
Location { id: 4, name: "코엑스".to_string(), position: LatLng { lat: 37.5120, lng: 127.0588 } },
])
}

// src/data_loader.rs
#[component]
fn LocationDataLoader(map_state: MapState) -> impl IntoView {
// create_resource를 사용하여 서버 함수를 호출한다.
let locations_resource = create_resource(|| (), |_| async move { get_locations().await });

view! {
// Suspense는 리소스가 로딩 중일 때 fallback UI를 보여준다.
<Suspense fallback=move |

| view! { <p>"Loading locations..."</p> }>
{move |

| {
locations_resource.get().map(|result| {
match result {
Ok(locations) => {
// 데이터 로딩이 완료되면 locations Signal을 업데이트한다.
// Effect 밖에서 Signal을 업데이트하는 것은 안티패턴일 수 있으므로 주의.
// 여기서는 초기 데이터 로드 목적으로 사용한다.
map_state.locations.set(locations);
view! {}.into_view() // UI를 직접 렌더링하지는 않는다.
}
Err(e) => view! { <p>{format!("Error loading locations: {}", e)}</p> }.into_view(),
}
})
}}
</Suspense>
}
}

<Suspense> 컴포넌트는 내부에 있는 resource가 로딩 상태(None)일 때 fallback으로 지정된 UI를 렌더링한다. 리소스 로딩이 완료되면(Some(...)), 자식 클로저가 실행되어 결과를 처리한다. 이 패턴을 통해 비동기 데이터 로딩 상태를 선언적으로 쉽게 처리할 수 있다.

반면, 사용자가 버튼을 클릭하여 특정 위치를 저장하는 것과 같은 ‘쓰기’ 작업에는 create_action이 더 적합하다. create_action.dispatch()를 통해 수동으로 트리거되며, 작업의 진행 상태(pending)와 결과(value)를 추적할 수 있는 Signal을 제공한다.22

4.2 라우팅 기반 지도 뷰 구현

복잡한 애플리케이션에서는 URL 경로에 따라 다른 콘텐츠를 보여주는 라우팅 기능이 필수적이다. leptos_router는 Leptos 애플리케이션에 클라이언트 사이드 라우팅을 쉽게 통합할 수 있게 해준다.32 이를 활용하면 특정 위치의 상세 정보를 보여주는 URL(예: /locations/3)을 만들고, URL 변경에 따라 지도 뷰를 동적으로 업데이트할 수 있다.

먼저, leptos_router 의존성을 추가하고 App 컴포넌트를 <Router>로 감싼다.

Rust

// src/app.rs
use leptos_router::*;

#[component]
fn App() -> impl IntoView {
let map_state = MapState::new();
provide_context(map_state);

view! {
<Router>
<main>
<Routes>
<Route path="/" view=move |

| view! { <HomePage /> } />
</Routes>
</main>
</Router>
}
}

#[component]
fn HomePage() -> impl IntoView {
let map_state = use_context::<MapState>().unwrap();
view! {
<h1>"Leptos Google Maps"</h1>
<LocationDataLoader map_state=map_state />
<MapController map_state=map_state />
<GoogleMap map_state=map_state />
}
}

이제 동적 라우트 파라미터를 사용하여 특정 위치를 보여주는 경로를 추가한다. /locations/:id와 같은 경로는 :id 부분에 어떤 값이든 올 수 있음을 의미한다. 컴포넌트 내에서는 use_params 훅을 사용하여 이 파라미터 값을 추출할 수 있다.33

Rust

// src/app.rs (Routes 내부 수정)
<Route path="/locations/:id" view=move |

| view! { <LocationDetailPage /> } />

// src/location_detail.rs
#[derive(Params, PartialEq)]
struct LocationParams {
id: u32,
}

#[component]
fn LocationDetailPage() -> impl IntoView {
let map_state = use_context::<MapState>().unwrap();
let params = use_params::<LocationParams>();

// params는 Memo<Result<...>> 타입이다.
let location_id = move |

| {
params.with(|params| params.as_ref().map(|p| p.id).ok())
};

// 라우트 파라미터가 변경될 때마다 지도의 중심을 해당 위치로 이동시킨다.
create_effect(move |_| {
if let Some(id) = location_id() {
// locations Signal에서 해당 ID의 위치를 찾는다.
let maybe_location = map_state.locations.get().into_iter().find(|loc| loc.id == id);
if let Some(location) = maybe_location {
map_state.center.set(location.position);
map_state.zoom.set(17);
}
}
});

view! {
//... 상세 정보 UI
}
}

use_params가 반환하는 값은 Memo로 감싸져 있어 URL이 변경될 때마다 자동으로 업데이트된다. create_effect는 이 Memo의 변화를 감지하여, 새로운 id에 해당하는 위치를 찾아 지도의 중심 좌표와 줌 레벨을 변경한다. 이로써 URL이 애플리케이션의 상태를 주도하는, 현대적인 웹 애플리케이션의 기본 동작을 구현할 수 있다.

4.3 정보 창(InfoWindow) 및 커스텀 오버레이

지도 위의 마커를 클릭했을 때 추가 정보를 보여주는 정보 창(InfoWindow)은 사용자 경험을 향상시키는 중요한 요소다.7 Google Maps API의 InfoWindow 객체를 생성하고 마커의 click 이벤트에 연결하여 이를 구현할 수 있다.

더 나아가, 정보 창 내부에 단순한 HTML 문자열이 아닌 동적인 Leptos 컴포넌트를 렌더링하는 고급 기법을 적용할 수 있다. 이는 두 세계를 더욱 깊이 통합하여, 정보 창 내부에서도 Leptos의 반응형 시스템을 그대로 활용할 수 있게 해준다.

이 기법의 핵심은 다음과 같다.

  1. InfoWindowcontent 옵션으로 비어있는 <div> 요소를 전달한다.
  2. InfoWindow가 열릴 때, 이 <div> DOM 요소에 접근한다.
  3. Leptos의 mount_to 함수를 사용하여 해당 <div>에 별도의 Leptos 컴포넌트 트리를 동적으로 마운트한다.

JavaScript

// js/maps_infowindow.js
export function createInfoWindow(options) {
return new google.maps.InfoWindow(options);
}

export function openInfoWindow(infoWindow, map, anchor) {
infoWindow.open({ map, anchor });
}

export function getInfoWindowContentElement(infoWindow) {
return infoWindow.getContent();
}

Rust

// src/map_marker.rs (수정)
//...
let marker = create_marker(&map, &js_options);

// 정보 창 생성
let info_window_content = document().create_element("div").unwrap();
let info_window = create_info_window(&json!({ "content": info_window_content }).into());

// 마커 클릭 이벤트 리스너
let click_closure = Closure::wrap(Box::new(move |

| {
// 정보 창 내부에 렌더링할 Leptos 컴포넌트
let info_view = view! {
<div>
<h3>{&location.name}</h3>
<p>{format!("Lat: {}, Lng: {}", location.position.lat, location.position.lng)}</p>
</div>
};
// 기존 content를 비우고 새로 마운트
let content_element = get_info_window_content_element(&info_window);
content_element.set_inner_html("");
info_view.mount_to(content_element.unchecked_into());

open_info_window(&info_window, &map, &marker);
}) as Box<dyn FnMut()>);

add_map_listener(&marker, "click", &click_closure);

on_cleanup(move |

| {
remove_marker(&marker);
click_closure.forget();
});
//...

이 방식은 Leptos의 렌더링 시스템을 Google Maps API의 DOM 관리 영역에 ’주입’하는 것과 같다. 이를 통해 정보 창 내부에서도 Signal을 사용하거나 복잡한 로직을 가진 컴포넌트를 재사용하는 등 Leptos의 모든 기능을 활용할 수 있게 된다.

4.4 프로덕션을 위한 최종 고려사항

개발을 마치고 애플리케이션을 배포하기 전, 보안과 최적화 측면에서 몇 가지 중요한 사항을 반드시 점검해야 한다.

  • API 키 보안 아키텍처: 1.2절에서 언급했듯이, 클라이언트 측 코드에 API 키를 직접 포함하는 것은 매우 위험하다. 프로덕션 환경에서는 Leptos의 서버 함수(#[server])를 사용하여 이 문제를 근본적으로 해결해야 한다.1

  • 서버 함수는 클라이언트 코드와 같은 위치에 작성되지만, 실제로는 서버에서만 실행된다.

  • API 키를 서버의 환경 변수나 비밀 관리 시스템에 저장하고, 클라이언트는 API 키가 필요 없는 서버 함수를 호출한다.

  • 서버 함수는 내부적으로 안전하게 저장된 API 키를 사용하여 Google Maps API와 통신하거나, 클라이언트가 지도를 초기화하는 데 필요한 설정 정보(API 키 포함)를 안전하게 전달한다.

Rust

#[server(GetMapConfig, "/api")]
pub async fn get_map_config() -> Result<String, ServerFnError> {
// 서버 환경 변수에서 API 키를 읽어온다.
let api_key = std::env::var("GOOGLE_MAPS_API_KEY")
.map_err(|e| ServerFnError::ServerError(e.to_string()))?;
Ok(api_key)
}

// 클라이언트 컴포넌트에서
let api_key_resource = create_resource(|| (), |_| get_map_config());
// 이 리소스가 resolve되면 그 값으로 API를 로드한다.
  • 빌드 및 배포: 개발이 완료되면 cargo leptos build --release 명령어를 사용하여 프로덕션용으로 최적화된 WASM 바이너리와 정적 에셋들을 생성한다. 생성된 site 디렉터리의 내용물을 Nginx, Caddy와 같은 정적 파일 서버나 Vercel, Netlify와 같은 호스팅 서비스에 배포하면 된다.

  • 대안 기술 고려: Google Maps API는 강력하지만 비용이 발생할 수 있으며, 특정 커스터마이징에 제약이 있을 수 있다. 프로젝트의 요구사항에 따라 오픈 소스 대안인 OpenStreetMap을 고려해볼 수 있다. OpenStreetMap은 Leaflet.js와 같은 라이브러리와 함께 사용되며, leptos-leaflet이라는 크레이트는 Leptos에서 Leaflet을 쉽게 사용할 수 있는 컴포넌트를 제공한다.35 비용, 데이터 소유권, 커스터마이징 자유도 등을 종합적으로 고려하여 기술 스택을 선택해야 한다.

5. 결론: Leptos와 JavaScript 생태계의 공존을 위한 아키텍처

본 보고서는 Rust 기반 웹 프레임워크 Leptos와 Google Maps JavaScript API를 통합하여 고성능 지도 애플리케이션을 구축하는 과정을 심층적으로 분석했다. 이 과정에서 우리는 단순한 API 연동을 넘어, 두 기술 스택의 근본적인 패러다임 차이를 극복하기 위한 핵심 아키텍처 패턴을 도출했다.

그 중심에는 ‘Reactive Map Controller’ 패턴이 있다. 이 패턴의 핵심 원칙은 다음과 같이 요약할 수 있다.

  1. 상태 소유권의 명확한 분리: 애플리케이션의 모든 핵심 상태(지도의 중심, 줌 레벨, 마커 목록 등)는 Leptos의 반응형 시스템, 즉 Signal이 ’진실의 원천’으로서 소유하고 관리한다. 외부 JavaScript 라이브러리는 이 상태를 반영하는 ’뷰(View)’의 역할에 충실해야 한다.
  2. 양방향 데이터 동기화 채널 구축: create_effectwatch_debounced를 사용하여 Leptos의 상태 변화를 Google Maps API의 명령형 메서드 호출로 변환하는 단방향 채널(Leptos -> GMaps)을 구축했다. 반대로, wasm-bindgenClosure를 활용하여 지도에서 발생하는 이벤트를 감지하고 Leptos의 Signal을 업데이트하는 역방향 채널(GMaps -> Leptos)을 구축함으로써 완전한 양방향 동기화를 구현했다.

또한, 우리는 WebAssembly와 JavaScript 사이의 경계를 넘나드는 통신에는 본질적인 성능 비용이 수반된다는 점을 확인했다. Leptos의 세분화된 반응성이 유발할 수 있는 과도한 API 호출을 방지하기 위해 디바운싱(debouncing)과 같은 최적화 전략을 신중하게 적용하는 것이 매우 중요함을 다시 한번 강조한다.

궁극적으로, 이 통합 과정은 Leptos와 같은 Rust-WASM 프레임워크가 기존 JavaScript 생태계를 대체하는 것이 아님을 명확히 보여준다. 오히려 이들은 수십 년간 축적된 JavaScript의 방대한 라이브러리와 자산을 Rust가 제공하는 타입 안정성, 메모리 안전성, 그리고 최상의 성능으로 ’지휘’하고 ’통합’하는 강력한 도구로서 기능한다. ‘Reactive Map Controller’ 패턴은 이러한 통합을 위한 하나의 청사진이며, 그 원칙은 지도뿐만 아니라 차트, 데이터 시각화, 리치 텍스트 에디터 등 다양한 외부 JavaScript 라이브러리와의 공존을 위한 견고한 기반이 될 것이다. 이는 Rust 기반 웹 개발이 나아갈 미래에 대한 구체적인 비전과 실질적인 방법론을 제시한다.

6. Works cited

  1. Leptos: Home, accessed October 27, 2025, https://leptos.dev/
  2. leptos-rs/leptos: Build fast web applications with Rust. - GitHub, accessed October 27, 2025, https://github.com/leptos-rs/leptos
  3. Leptos - GitHub, accessed October 27, 2025, https://github.com/leptos-rs
  4. Want a Web Framework for Rust, Not JavaScript? Try Leptos - The New Stack, accessed October 27, 2025, https://thenewstack.io/want-a-web-framework-for-rust-not-javascript-try-leptos/
  5. GitHub - gbj/leptos: A full-stack, isomorphic Rust web framework leveraging fine-grained reactivity to build declarative user interfaces. - Reddit, accessed October 27, 2025, https://www.reddit.com/r/rust/comments/y5scij/github_gbjleptos_a_fullstack_isomorphic_rust_web/
  6. Overview | Maps JavaScript API - Google for Developers, accessed October 27, 2025, https://developers.google.com/maps/documentation/javascript/overview
  7. Google Maps Platform Documentation | Maps JavaScript API - Google for Developers, accessed October 27, 2025, https://developers.google.com/maps/documentation/javascript
  8. Introduction to JavaScript and the Google Maps API - MichaelMinn.net, accessed October 27, 2025, https://michaelminn.net/tutorials/google-maps-api
  9. leptos - Rust - Docs.rs, accessed October 27, 2025, https://docs.rs/leptos/latest/leptos/
  10. Custom build process for leptos application using trunk build - Stack Overflow, accessed October 27, 2025, https://stackoverflow.com/questions/79626530/custom-build-process-for-leptos-application-using-trunk-build
  11. Tutorial docs/guide #364 - leptos-rs/leptos - GitHub, accessed October 27, 2025, https://github.com/leptos-rs/leptos/issues/364
  12. Getting Started - Leptos Book, accessed October 27, 2025, https://book.leptos.dev/getting_started/index.html
  13. Add a Google Map with a Marker using JavaScript | Maps JavaScript API, accessed October 27, 2025, https://developers.google.com/maps/documentation/javascript/adding-a-google-map
  14. Google Maps Platform security guidance | Google for Developers, accessed October 27, 2025, https://developers.google.com/maps/api-security-best-practices
  15. Best practices for securely using API keys - API Console Help - Google Help, accessed October 27, 2025, https://support.google.com/googleapi/answer/6310037?hl=en
  16. Best practices for managing API keys | Authentication - Google Cloud Documentation, accessed October 27, 2025, https://docs.cloud.google.com/docs/authentication/api-keys-best-practices
  17. Compiling from Rust to WebAssembly - WebAssembly | MDN, accessed October 27, 2025, https://developer.mozilla.org/en-US/docs/WebAssembly/Guides/Rust_to_Wasm
  18. Integrating with JavaScript: wasm-bindgen, web_sys, and …, accessed October 27, 2025, https://book.leptos.dev/web_sys.html
  19. Leptos & wasm-bindgen - Josiah Parry, accessed October 27, 2025, https://josiahparry.com/posts/2024-01-15
  20. A Basic Component - Leptos Book, accessed October 27, 2025, https://book.leptos.dev/view/01_basic_component.html
  21. component in leptos - Rust - Docs.rs, accessed October 27, 2025, https://docs.rs/leptos/latest/leptos/attr.component.html
  22. Loading Data with Resources - Leptos Book, accessed October 27, 2025, https://book.leptos.dev/async/10_resources.html
  23. Global State Management - Leptos Book, accessed October 27, 2025, https://book.leptos.dev/15_global_state.html
  24. Working with Signals - Leptos Book, accessed October 27, 2025, https://book.leptos.dev/reactivity/working_with_signals.html
  25. ASP.NET Core Blazor JavaScript interoperability (JS interop) performance best practices, accessed October 27, 2025, https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/javascript-interoperability?view=aspnetcore-9.0
  26. Functions - Leptos-Use Guide, accessed October 27, 2025, https://leptos-use.rs/functions.html
  27. Events | Maps JavaScript API | Google for Developers, accessed October 27, 2025, https://developers.google.com/maps/documentation/javascript/events
  28. Working with Events | Google Maps API Succinctly | Syncfusion®, accessed October 27, 2025, https://www.syncfusion.com/succinctly-free-ebooks/google-maps-api-succinctly/working-with-events
  29. use_geolocation - Leptos-Use Guide, accessed October 27, 2025, https://leptos-use.rs/sensors/use_geolocation.html
  30. Resource in leptos::prelude - Rust - Docs.rs, accessed October 27, 2025, https://docs.rs/leptos/latest/leptos/prelude/struct.Resource.html
  31. Actions - Leptos Book, accessed October 27, 2025, https://book.leptos.dev/async/13_actions.html
  32. Defining
  33. Params and Queries - Leptos Book, accessed October 27, 2025, https://book.leptos.dev/router/18_params_and_queries.html
  34. Nested Routing - Leptos Book, accessed October 27, 2025, https://book.leptos.dev/router/17_nested_routing.html
  35. leptos_leaflet - Rust - Docs.rs, accessed October 27, 2025, https://docs.rs/leptos-leaflet
  36. Deploying your own Slippy Map - OpenStreetMap Wiki, accessed October 27, 2025, https://wiki.openstreetmap.org/wiki/Deploying_your_own_Slippy_Map